在完成了 API 串接與圖表繪製後,我們的 AI 智慧個人財務追蹤器 (Smart Finance Tracker) 已經具備了完整的骨架與肌肉。但要讓使用者感覺這是一款「現代、靈動且有質感」的產品,還差最後臨門一腳 —— 微互動 (Micro-interactions) 與流暢動畫。今天我們引入 React 生態系最強大的動畫庫 Framer Motion!
為財務面板(Dashboard)中的卡片(Stat Cards)與交易列表加入進場淡入與滑入動畫(Entrance Animations)。
為新增交易按鈕與互動元件加入微互動(Hover, Tap Scale Feedback)。
解決 AI 在處理 Framer Motion layout 動畫與動態列表刪除時常見的 DOM 跳動(Jank)與 Key 綁定問題。
Framer Motion 的語法非常直覺,但要做出「彈性恰到好處(Spring Physics)」而非「生硬遲鈍」的動畫,參數調整才是精髓所在。
給 AI 的指令(Prompt)
我們正在 Next.js + Tailwind CSS 專案中調整 UI 質感。
請幫我用 Framer Motion 重構交易紀錄列表元件 TransactionList.tsx。
要求:
AI 回得非常迅速,幾秒鐘就寫好了一個套用 motion.div 與 AnimatePresence 的元件:
// src/components/TransactionList.tsx
'use client';
import { motion, AnimatePresence } from 'framer-motion';
import { Transaction } from '@/hooks/useTransactions';
export const TransactionList = ({ transactions }: { transactions: Transaction[] }) => {
return (
<div className="space-y-3">
<AnimatePresence>
{transactions.map((tx) => (
<motion.div
key={tx.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="p-4 bg-slate-800 rounded-xl flex justify-between items-center shadow-md"
>
<div>
<p className="font-semibold text-white">{tx.note}</p>
<p className="text-sm text-slate-400">{tx.category}</p>
</div>
<span className={tx.type === 'EXPENSE' ? 'text-rose-400' : 'text-emerald-400'}>
{tx.type === 'EXPENSE' ? '-' : '+'}${tx.amount}
</span>
</motion.div>
))}
</AnimatePresence>
</div>
);
};
將這段 Code 放上頁面測試時,視覺效果雖然看起來「動起來了」,但細看卻有很多令人不適的 UI 瑕疵:
刪除時卡片「硬跳」沒有平滑補位 (Layout Shift):
當刪除中間某一筆交易時,雖然該卡片成功淡出了,但下方卡片卻是「硬生生直接跳上來」,完全沒有平滑補位的動畫!這是因為 AI 忘記給 motion.div 加上 Framer Motion 靈魂屬性:layout。
缺乏「交錯 (Stagger)」體感,百萬卡片齊刷刷彈出:
AI 雖然收到了「交錯進場」的指令,但它直接把 initial 和 animate 寫死在子卡片上,沒有使用 variants 搭配 staggerChildren,導致所有列表卡片在同一個時間點瞬間彈出,視覺非常雜亂。
Hover 導致邊框模糊與內容抖動:
whileHover={{ scale: 1.02 }} 雖然實現了放大,但直接改變 scale 會導致向量字體與內部邊框在特定螢幕解析度下出現模糊(Subpixel Rendering Bug)。
將問題整理成標準的第二輪診斷指令:
修正 Prompt:
「1. 請改用 variants 父子架構,在父層容器定義 staggerChildren: 0.08,實現流暢的交錯入場。
2. 請在子卡片 <motion.div> 加上 layout 屬性,讓列表增刪時周圍元件能平滑滑動補位。
3. 將 whileHover 從 scale: 1.02 改為以 y: -2 與 shadow 變化為主,避免字體模糊。」
經過二輪優化後的最終版程式碼,不僅性能優異,質感更是直接媲美頂級 SaaS 產品:
'use client';
import { motion, AnimatePresence } from 'framer-motion';
import { Transaction } from '@/hooks/useTransactions';
// 父層容器:控制子元素的交錯進場時間
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.06, // 每項間隔 0.06 秒
},
},
};
// 子卡片動畫:物理彈簧體感
const itemVariants = {
hidden: { opacity: 0, y: 15 },
visible: {
opacity: 1,
y: 0,
transition: { type: 'spring', stiffness: 300, damping: 24 },
},
exit: {
opacity: 0,
scale: 0.95,
transition: { duration: 0.2 },
},
};
interface Props {
transactions: Transaction[];
onDelete?: (id: string) => void;
}
export const TransactionList = ({ transactions, onDelete }: Props) => {
return (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-3"
>
<AnimatePresence mode="popLayout">
{transactions.map((tx) => (
<motion.div
key={tx.id}
layout // 關鍵:讓列表變動時,周圍卡片自動平滑位移
variants={itemVariants}
initial="hidden"
animate="visible"
exit="exit"
whileHover={{ y: -2, boxShadow: '0px 10px 20px rgba(0, 0, 0, 0.2)' }}
whileTap={{ scale: 0.98 }}
className="p-4 bg-slate-800/80 backdrop-blur border border-slate-700/50 rounded-xl flex justify-between items-center transition-colors hover:border-slate-600"
>
<div>
<p className="font-medium text-slate-100">{tx.note}</p>
<p className="text-xs text-slate-400 mt-0.5">{tx.category}</p>
</div>
<div className="flex items-center gap-3">
<span className={`font-semibold ${tx.type === 'EXPENSE' ? 'text-rose-400' : 'text-emerald-400'}`}>
{tx.type === 'EXPENSE' ? '-' : '+'}${tx.amount.toLocaleString()}
</span>
{onDelete && (
<button
onClick={() => onDelete(tx.id)}
className="text-slate-500 hover:text-slate-300 text-xs px-2 py-1 rounded transition-colors"
>
刪除
</button>
)}
</div>
</motion.div>
))}
</AnimatePresence>
</motion.div>
);
};
FLIP 動畫原理與 layout 屬性:
Framer Motion 的 layout 屬性底層使用了 FLIP (First, Last, Invert, Play) 繪圖模式。它會在 DOM 改變前後計算位置差,並用高效率的 transform 補齊過渡動畫,避免瀏覽器觸發昂貴的 Reflow(重排)。
AnimatePresence mode="popLayout":
在 React 中,當元件被 Unmount(從 DOM 移除)時,預設是無法播放離場動畫的。AnimatePresence 能暫存即將消失的 DOM,而 mode="popLayout" 則能防止離場元素佔用排版空間,解決卡片刪除時的卡頓感。